/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import static android.persist.op.sqlite.SQLLogging.SQL_LOG_TAG;

import java.util.Map;
import java.util.Map.Entry;

import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;
import android.persist.op.Delete;
import android.persist.op.OpException;
import android.util.Log;

/**
 * {@code SQLiteDelete} is an operation that deletes a class' data from the
 * underlying SQLite database. A
 * "DELETE FROM 'tablename' WHERE <key value pairs>" is executed to perform the
 * deletion.
 * 
 * @author Richard Schilling
 * @param <OT> the type of class that this operation works on.
 * @since 1.0
 */
public class SQLiteDelete<OT> extends Delete<OT> {

    /**
     * The database that the data is deleted from.
     */
    private final SQLiteDatabase mDatabase;

    /**
     * Class configuration object for {@link operand}'s type.
     */
    private final ClassConfig<OT> mConfig;

    /**
     * Contains the number of rows in the database affected by invoking
     * {@link #call()}.
     */
    private long mRowsAffected = Long.MIN_VALUE;

    /**
     * Creates a new instance of this operation
     * 
     * @param db the database to apply the operation against.
     * @param o the object to delete from the data store.
     * @since 1.0
     * @throws IllegalArgumentException if db or o is null; if any key value in
     *             o is null.
     * @throws OpException if db is not writeable.
     */
    @SuppressWarnings("unchecked")
    public SQLiteDelete(SQLiteDatabase db, OT o) {
        super(o);

        if (db == null) {
            throw new IllegalArgumentException("db cannot be null");
        }

        if (db.isReadOnly()) {
            throw new OpException("database must be writeable");
        }

        if (o == null) {
            throw new IllegalArgumentException("o cannot be null");
        }

        mDatabase = db;
        mConfig = (ClassConfig<OT>) ClassConfig.getConfig((Class<OT>) o.getClass());

        Map<FieldConfig, Object> keyValues = mConfig.getKeyValues(o);
        for (Entry<FieldConfig, Object> entry : keyValues.entrySet()) {
            if (entry.getValue() == null) {
                throw new IllegalArgumentException(String.format(
                        "key '%s' in object '%s' is null.", entry.getKey().getPersistedName(),
                        operand.toString()));
            }
        }

    }

    /**
     * Deletes from the data store the object passed to the constructor.
     * 
     * @author Richard Schilling
     * @since 1.0
     * @return true if the number of rows affected are > 0.
     * @throws SQLiteException if the SQL operation fails
     * @throws OpException if the database is closed.
     */
    @Override
    public Boolean call() {
        if (!mDatabase.isOpen()) {
            throw new OpException("database is closed");
        }

        String where = SQLiteUtil.generateWhere(operand);

        StringBuilder idQuery = new StringBuilder();
        idQuery.append("SELECT ROWID FROM '");
        idQuery.append(mConfig.storageName);
        idQuery.append("' WHERE ");
        idQuery.append(where);

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    idQuery.toString());
        }
        SQLiteStatement idStatement = mDatabase.compileStatement(idQuery.toString());
        long rowID = idStatement.simpleQueryForLong();

        /**
         * Delete records from collection child tables.
         */
        for (FieldConfig fConfig : mConfig.foreignFields) {
            if (!fConfig.isCollection()) {
                continue;
            }

            StringBuilder childQuery = new StringBuilder();

            childQuery.append("DELETE FROM '");
            childQuery.append(mConfig.storageName);
            childQuery.append("_");
            childQuery.append(fConfig.getPersistedName());
            childQuery.append("' WHERE parent_ROWID=");
            childQuery.append(Long.toString(rowID));

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        childQuery.toString());
            }
            mDatabase.execSQL(childQuery.toString());

        }

        StringBuilder deleteQuery = new StringBuilder();
        deleteQuery = new StringBuilder();
        deleteQuery.append("DELETE FROM '");
        deleteQuery.append(mConfig.storageName);
        deleteQuery.append("' WHERE ");
        deleteQuery.append(where);

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    deleteQuery.toString());
        }
        mRowsAffected = mDatabase.compileStatement(deleteQuery.toString()).simpleQueryForLong();

        return mRowsAffected > 0;

    }

    /**
     * Returns the number of rows affected by this operation. Invoking this
     * function after {@link #call()} will tell you how many rows were affected
     * by the deletion.
     * 
     * @return the number of rows affected by the deletion.
     * @throws UnsupportedOperationException if {@link #call()} was not invoked
     *             before this function.
     * @since 1.0
     */
    public long getRowsAffected() {
        if (mRowsAffected == Long.MIN_VALUE) {
            throw new UnsupportedOperationException("call() has not been invoked yet.");
        }
        return mRowsAffected;
    }

}
